﻿using System;
using System.IO;
using System.Numerics;
using OpenSage.Content;
using OpenSage.Graphics.Cameras;
using OpenSage.Gui;
using OpenSage.Logic.Object;
using OpenSage.Mathematics;
using OpenSage.Terrain;
using Veldrid;

namespace OpenSage;

public class RadarDrawUtil
{
    private readonly Radar _radar;
    private readonly HeightMap _heightMap;
    private readonly IGameObjectCollection _gameObjectCollection;
    private readonly Camera _camera;

    private readonly Texture _miniMapTexture;
    private readonly MappedImage _heroImage;

    public RadarDrawUtil(Radar radar, HeightMap heightMap, IGameObjectCollection gameObjectCollection, Camera camera, AssetStore assetStore, string mapPath)
    {
        _radar = radar;
        _heightMap = heightMap;
        _gameObjectCollection = gameObjectCollection;
        _camera = camera;

        if (mapPath != null)
        {
            var basePath = Path.Combine(Path.GetDirectoryName(mapPath), Path.GetFileNameWithoutExtension(mapPath));

            // Minimap images drawn by an artist
            var mapArtPath = basePath + "_art.tga";
            _miniMapTexture = assetStore.GuiTextures.GetByName(mapArtPath)?.Texture;

            if (_miniMapTexture == null)
            {
                // Fallback to minimap images generated by WorldBuilder
                mapArtPath = basePath + ".tga";
                _miniMapTexture = assetStore.GuiTextures.GetByName(mapArtPath)?.Texture;
            }
        }

        _heroImage = assetStore.MappedImages.GetByName("HeroReticle");
    }

    public Vector3 RadarToWorldSpace(Point2D mousePosition, in Mathematics.Rectangle destinationRectangle)
    {
        var miniMapTransform = RectangleF.CalculateTransformForRectangleFittingAspectRatio(
            new RectangleF(0, 0, _miniMapTexture.Width, _miniMapTexture.Height),
            new SizeF(_miniMapTexture.Width, _miniMapTexture.Height),
            destinationRectangle.Size);

        Matrix3x2.Invert(miniMapTransform, out var miniMapTransformInverse);

        // Transform by inverse of miniMapTransform
        var position2D = Vector2.Transform(
            mousePosition.ToVector2(),
            miniMapTransformInverse);

        // Divide by minimap texture size.
        position2D.X /= _miniMapTexture.Width;
        position2D.Y /= _miniMapTexture.Height;

        // Clamp coordinates to [0, 1] to avoid out of bounds errors.
        position2D.X = Math.Clamp(position2D.X, 0f, 1f);
        position2D.Y = Math.Clamp(position2D.Y, 0f, 1f);

        // Multiply position by map size.
        position2D.X *= _heightMap.Width;
        position2D.Y *= _heightMap.Height;

        // Invert y.
        position2D.Y = _heightMap.Height - position2D.Y;

        return _heightMap.GetPosition((int)position2D.X, (int)position2D.Y);
    }

    public void Draw(DrawingContext2D drawingContext, in Mathematics.Rectangle destinationRectangle)
    {
        // TODO: Don't draw minimap if player doesn't have radar.

        if (_miniMapTexture == null)
        {
            return;
        }

        var fittedRectangle = RectangleF.CalculateRectangleFittingAspectRatio(
            new RectangleF(0, 0, _miniMapTexture.Width, _miniMapTexture.Height),
            new SizeF(_miniMapTexture.Width, _miniMapTexture.Height),
            destinationRectangle.Size);

        DrawRadarMinimap(drawingContext, fittedRectangle);

        var objectTransform = RectangleF.CalculateTransformForRectangleFittingAspectRatio(
            new RectangleF(0, 0, _miniMapTexture.Width, _miniMapTexture.Height),
            new SizeF(_miniMapTexture.Width, _miniMapTexture.Height),
            destinationRectangle.Size);

        DrawRadarOverlay(drawingContext, objectTransform);
    }

    public void DrawRadarMinimap(DrawingContext2D drawingContext, in Mathematics.Rectangle rectangle, bool flip = false)
    {
        drawingContext.DrawImage(_miniMapTexture, null, rectangle, flip);
    }

    public void DrawRadarOverlay(DrawingContext2D drawingContext, in Mathematics.Rectangle rectangle)
    {
        var rectF = rectangle.ToRectangleF();
        var objectTransform = RectangleF.CalculateTransformForRectangleFittingAspectRatio(
            rectF,
            rectF.Size,
            rectangle.Size);
        DrawRadarOverlay(drawingContext, objectTransform);
    }

    public void DrawRadarOverlay(DrawingContext2D drawingContext, in Matrix3x2 transform)
    {
        foreach (var item in _radar.VisibleItems)
        {
            DrawRadarItem(item, drawingContext, transform);
        }

        DrawCameraFrustum(drawingContext, transform);
    }

    private void DrawRadarItem(
        RadarItem item,
        DrawingContext2D drawingContext,
        in Matrix3x2 miniMapTransform)
    {
        // TODO: Use RadarPriority to decide what gets shown when there are multiple
        // things in the same radar position.

        var gameObject = _gameObjectCollection.GetObjectById(item.ObjectId);

        if (gameObject is null)
        {
            return;
        }

        var gameObjectPosition = gameObject.Translation;

        var radarPosition = WorldToRadarSpace(gameObjectPosition, miniMapTransform);
        if (radarPosition == null)
        {
            return;
        }

        // TODO: Use actual object geometry.
        var gameObjectRectangle = new Mathematics.Rectangle(
            (int)radarPosition.Value.X,
            (int)radarPosition.Value.Y,
            2, 2);

        drawingContext.FillRectangle(
            gameObjectRectangle,
            item.Color.ToColorRgbaF());

        if (gameObject.IsKindOf(ObjectKinds.Hero) && _heroImage != null)
        {
            var startX = gameObjectRectangle.X + 1 - _heroImage.Coords.Width / 2f;
            var startY = gameObjectRectangle.Y + 1 - _heroImage.Coords.Height / 2f;
            var heroRectangle = new RectangleF(startX, startY, _heroImage.Coords.Width, _heroImage.Coords.Height);
            drawingContext.DrawMappedImage(_heroImage, heroRectangle);
        }
    }

    private Vector2? WorldToRadarSpace(in Vector3 worldPosition, in Matrix3x2 miniMapTransform)
    {
        var position2D = _heightMap.GetHeightMapPosition(worldPosition);

        // Invert y.
        position2D.Y = _heightMap.Height - position2D.Y;

        // Divide position by map size.
        position2D.X /= _heightMap.Width;
        position2D.Y /= _heightMap.Height;

        // Multiply by minimap texture size.
        position2D.X *= _miniMapTexture.Width;
        position2D.Y *= _miniMapTexture.Height;

        // Transform by minimapTransform
        return Vector2.Transform(position2D, miniMapTransform);
    }

    private void DrawCameraFrustum(DrawingContext2D drawingContext, Matrix3x2 miniMapTransform)
    {
        // Create rays from camera position through each corner of the near plane.
        var frustumCorners = _camera.BoundingFrustum.Corners;
        var cameraPosition = _camera.Position;

        var groundPlane = new Plane(Vector3.UnitZ, 0);

        Vector3? GetGroundIntersectionPoint(int index)
        {
            var ray = new Ray(cameraPosition, Vector3.Normalize(frustumCorners[index] - cameraPosition));

            var distance = ray.Intersects(groundPlane);
            if (distance == null)
            {
                return null;
            }

            return ray.Position + ray.Direction * distance.Value;
        }

        var terrain0 = GetGroundIntersectionPoint(0);
        var terrain1 = GetGroundIntersectionPoint(1);
        var terrain2 = GetGroundIntersectionPoint(2);
        var terrain3 = GetGroundIntersectionPoint(3);

        if (terrain0 == null || terrain1 == null || terrain2 == null || terrain3 == null)
        {
            return;
        }

        void DrawFrustumLine(in Vector3 v0, in Vector3 v1)
        {
            var v0Radar = WorldToRadarSpace(v0, miniMapTransform);
            var v1Radar = WorldToRadarSpace(v1, miniMapTransform);

            if (v0Radar == null || v1Radar == null)
            {
                return;
            }

            // TODO: Clip lines to destination rectangle.

            drawingContext.DrawLine(
                new Line2D(v0Radar.Value, v1Radar.Value),
                2,
                new ColorRgbaF(1, 1, 0, 1));
        }

        DrawFrustumLine(terrain0.Value, terrain1.Value);
        DrawFrustumLine(terrain1.Value, terrain2.Value);
        DrawFrustumLine(terrain2.Value, terrain3.Value);
        DrawFrustumLine(terrain3.Value, terrain0.Value);
    }
}
